using System;
using System.IO;
using System.Collections.Generic;

namespace TextMenus
{
    /// <summary>
    /// An interactive I/O (console or file) menu.
    /// Lists various options and accepts customized input
    /// </summary>
	public class TextMenu
	{
        private static readonly MenuOption ExitOption = new MenuOption("Exit", Do_Exit);

		private static void Do_Exit(MenuInputInfo obj)
        { }

        private static string DEFAULT_HEADER = "MENU";
		private static string DEFAULT_FOOTER = String.Empty;
		private static string DEFAULT_PROMPT = "Enter your selection: ";

		/// <summary>
		/// A string which will be displayed above the menu options whenever
		/// the menu is run
		/// </summary>
		public string header;

		/// <summary>
		/// A string which will be displayed below the menu options whenever
		/// the menu is run
		/// </summary>
		public string footer;

		/// <summary>
		/// A string used to prompt the user to enter his selection
		/// </summary>
		public string prompt;

        /// <summary>
        /// Event which is invoked when the user chooses an option from the menu,
        /// before invoking the delegate bound to the option itself
        /// </summary>
        public event Action<MenuInputInfo> BeforeMenuOptionChosen;

        /// <summary>
        /// Event which is invoked when the user chooses an option from the menu,
        /// after invoking the delegate bound to the option itself
        /// </summary>
        public event Action<MenuInputInfo> AfterMenuOptionChosen;

		private int counter;
		private IDictionary<int,MenuOption> actionMap;

        protected TextReader input;
        protected TextWriter output;

        public TextMenu(bool withExitOption = true)
        {
            actionMap = new Dictionary<int, MenuOption>();
            header = DEFAULT_HEADER;
            footer = DEFAULT_FOOTER;
            prompt = DEFAULT_PROMPT;
            input = Console.In;
            output = Console.Out;
            InitializeInstance(withExitOption);
        }

        private void InitializeInstance(bool withExitOption)
        {
            BeforeMenuOptionChosen += DoNothing;
            AfterMenuOptionChosen += DoNothing;
            counter = 0;

            if (withExitOption)
                AddExitOption();
        }

        /// <summary>
        /// Sets the input stream
        /// </summary>
        public TextReader Input
        {
            set { input = value; }
        }

        /// <summary>
        /// Sets the output stream
        /// </summary>
        public TextWriter Output
        {
            set { output = value; }
        }

        private static void DoNothing(MenuInputInfo i)
        { }

		public void AddMenuOption (MenuOption option)
		{
			actionMap[counter++] = option;
		}

        internal MenuOption MatchMenuOption(int id)
        {
            return actionMap[id];
        }

        /// <summary>
        /// Adds a special "exit" option to the menu
        /// </summary>
        public void AddExitOption()
        {
            AddMenuOption(ExitOption);
        }

		public void RemoveMenuOption (MenuOption option)
		{
			foreach (KeyValuePair<int,MenuOption> kv in actionMap)
			{
				if (kv.Value == option)
				{
					actionMap.Remove(kv);
					break;
				}
			}
		}

        /// <summary>
        /// Run the menu continuously until the user selects the "Exit" option.
        /// Events and MenuOption delegates are invoked each time the user chooses an option.
        /// </summary>
        public void RunUntilExit()
        {
            MenuInputInfo inp;

            do
            {
            tryGetInput:
                DisplayMenu();

                try {
                    inp = new MenuInputInfo(this, input.ReadLine());
                } catch (Exception ex) {
                    if (ex is FormatException || ex is KeyNotFoundException)
                    {
                        DisplayInvalidInputMessage();
                        goto tryGetInput;
                    }
                    else
                        throw new Exception("Unexpected Exception Type", ex);
                }

                BeforeMenuOptionChosen.Invoke(inp);
                inp.SelectedOption.Invoke(inp);
                AfterMenuOptionChosen.Invoke(inp);

            } while (inp.SelectedOption != TextMenu.ExitOption);
        }

        /// <summary>
        /// Run the menu to obtain a single result, WITHOUT invoking any events or delegates
        /// </summary>
		public MenuInputInfo RunOnce ()
		{
			MenuInputInfo inp;

			DisplayMenu ();
			
			try {
                inp = new MenuInputInfo(this, input.ReadLine());
			} catch (Exception ex) {
				if (ex is FormatException || ex is KeyNotFoundException)
				{
					DisplayInvalidInputMessage();
                    return null;
				}
				else
					throw new Exception("Unexpected Exception Type",ex);
			}

            return inp;
		}

		protected virtual void DisplayInvalidInputMessage()
		{
			output.WriteLine ("Invalid Selection; Try Again");
		}

		protected virtual void DisplayMenu(bool displayPrompt = true)
		{
			output.WriteLine (header);

			foreach (KeyValuePair<int,MenuOption> kv in actionMap)
				output.WriteLine("{0} - {1}", kv.Key, kv.Value);

			output.WriteLine (footer);

            if (displayPrompt)
                output.Write(prompt);
		}
	}
}

